// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package main

import (
	"cmd/internal/archive"
	"fmt"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"
)

const usageMessage = `Usage: pack op file.a [name....]
Where op is one of cprtx optionally followed by v for verbose output.
For compatibility with old Go build environments the op string grc is
accepted as a synonym for c.

For more information, run
	go doc cmd/pack`

func usage() {
	fmt.Fprintln(os.Stderr, usageMessage)
	os.Exit(2)
}

func main() {
	log.SetFlags(0)
	log.SetPrefix("pack: ")
	// 至少需要“打包存档”。
	if len(os.Args) < 3 {
		log.Print("not enough arguments")
		fmt.Fprintln(os.Stderr)
		usage()
	}
	setOp(os.Args[1])
	var ar *Archive
	switch op {
	case 'p':
		ar = openArchive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.printContents)
	case 'r':
		ar = openArchive(os.Args[2], os.O_RDWR|os.O_CREATE, os.Args[3:])
		ar.addFiles()
	case 'c':
		ar = openArchive(os.Args[2], os.O_RDWR|os.O_TRUNC|os.O_CREATE, os.Args[3:])
		ar.addPkgdef()
		ar.addFiles()
	case 't':
		ar = openArchive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.tableOfContents)
	case 'x':
		ar = openArchive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.extractContents)
	default:
		log.Printf("invalid operation %q", os.Args[1])
		fmt.Fprintln(os.Stderr)
		usage()
	}
	if len(ar.files) > 0 {
		log.Fatalf("file %q not in archive", ar.files[0])
	}
}

// 不寻常的血统意味着争论不符合标准。
// 这些变量保存第一个参数指定的解码操作。
// op持有我们正在进行的操作（prtx）。
// 详细说明是否指定了“v”选项。
var (
	op      rune
	verbose bool
)

// setOp解析操作字符串（第一个参数）。
func setOp(arg string) {
	// 认识“go tool pack grc”，因为这是
	// 以前从一组输入文件构建新存档的标准方式
	// 的一种。接受它可以让旧的
	// 构建同时使用Go 1.2和Go 1.3的系统。
	if arg == "grc" {
		arg = "c"
	}

	for _, r := range arg {
		switch r {
		case 'c', 'p', 'r', 't', 'x':
			if op != 0 {
				// 最多可以设置一个。
				usage()
			}
			op = r
		case 'v':
			if verbose {
				// 只能设置一次。
				usage()
			}
			verbose = true
		default:
			usage()
		}
	}
}

const (
	arHeader = "!<arch>\n"
)

// 存档代表打开的存档文件。始终从开始到结束按顺序扫描
// 而不备份。
type Archive struct {
	a        *archive.Archive
	files    []string // 要处理的文件的明确列表。
	pad      int      // 在当前存档文件末尾填充所需字节
	matchAll bool     // 匹配存档中的所有文件
}

// 存档打开（并在必要时创建）命名存档。
func openArchive(name string, mode int, files []string) *Archive {
	f, err := os.OpenFile(name, mode, 0666)
	if err != nil {
		log.Fatal(err)
	}
	var a *archive.Archive
	if mode&os.O_TRUNC != 0 { // c命令
		a, err = archive.New(f)
	} else {
		a, err = archive.Parse(f, verbose)
		if err != nil && mode&os.O_CREATE != 0 { // r命令
			a, err = archive.New(f)
		}
	}
	if err != nil {
		log.Fatal(err)
	}
	return &Archive{
		a:        a,
		files:    files,
		matchAll: len(files) == 0,
	}
}

// 扫描归档文件并对每个条目执行指定的操作。
func (ar *Archive) scan(action func(*archive.Entry)) {
	for i := range ar.a.Entries {
		e := &ar.a.Entries[i]
		action(e)
	}
}

// listEntry在标准输出中打印一行描述条目。
func listEntry(e *archive.Entry, verbose bool) {
	if verbose {
		fmt.Fprintf(stdout, "%s\n", e.String())
	} else {
		fmt.Fprintf(stdout, "%s\n", e.Name)
	}
}

// 输出将条目复制到指定的写入程序。
func (ar *Archive) output(e *archive.Entry, w io.Writer) {
	r := io.NewSectionReader(ar.a.File(), e.Offset, e.Size)
	n, err := io.Copy(w, r)
	if err != nil {
		log.Fatal(err)
	}
	if n != e.Size {
		log.Fatal("short file")
	}
}

// match报告条目是否与参数列表匹配。
// 如果这样做，它还会从待处理列表中删除该文件。
func (ar *Archive) match(e *archive.Entry) bool {
	if ar.matchAll {
		return true
	}
	for i, name := range ar.files {
		if e.Name == name {
			copy(ar.files[i:], ar.files[i+1:])
			ar.files = ar.files[:len(ar.files)-1]
			return true
		}
	}
	return false
}

// addFiles将文件添加到存档中。该档案已知为
// sane，我们位于最后。
// 未尝试检查现有文件。
func (ar *Archive) addFiles() {
	if len(ar.files) == 0 {
		usage()
	}
	for _, file := range ar.files {
		if verbose {
			fmt.Printf("%s\n", file)
		}

		f, err := os.Open(file)
		if err != nil {
			log.Fatal(err)
		}
		aro, err := archive.Parse(f, false)
		if err != nil || !isGoCompilerObjFile(aro) {
			f.Seek(0, io.SeekStart)
			ar.addFile(f)
			goto close
		}

		for _, e := range aro.Entries {
			if e.Type != archive.EntryGoObj || e.Name != "_go_.o" {
				continue
			}
			ar.a.AddEntry(archive.EntryGoObj, filepath.Base(file), 0, 0, 0, 0644, e.Size, io.NewSectionReader(f, e.Offset, e.Size))
		}
	close:
		f.Close()
	}
	ar.files = nil
}

// FileLike抽象了我们需要的几个方法，因此我们可以在不需要真实文件的情况下进行测试。
type FileLike interface {
	Name() string
	Stat() (fs.FileInfo, error)
	Read([]byte) (int, error)
	Close() error
}

// addFile将单个文件添加到存档
func (ar *Archive) addFile(fd FileLike) {
	// 格式化条目。
	// 首先，获取它的信息。
	info, err := fd.Stat()
	if err != nil {
		log.Fatal(err)
	}
	// mtime、uid、gid都为零，因此重复构建会产生相同的输出。
	mtime := int64(0)
	uid := 0
	gid := 0
	ar.a.AddEntry(archive.EntryNativeObj, info.Name(), mtime, uid, gid, info.Mode(), info.Size(), fd)
}

// addPkgdef添加_;。将PKGDEF文件复制到存档中，从文件列表上的第一个Go对象文件复制
// 如果有的话。
// 已知档案为空。
func (ar *Archive) addPkgdef() {
	done := false
	for _, file := range ar.files {
		f, err := os.Open(file)
		if err != nil {
			log.Fatal(err)
		}
		aro, err := archive.Parse(f, false)
		if err != nil || !isGoCompilerObjFile(aro) {
			goto close
		}

		for _, e := range aro.Entries {
			if e.Type != archive.EntryPkgDef {
				continue
			}
			if verbose {
				fmt.Printf("__.PKGDEF # %s\n", file)
			}
			ar.a.AddEntry(archive.EntryPkgDef, "__.PKGDEF", 0, 0, 0, 0644, e.Size, io.NewSectionReader(f, e.Offset, e.Size))
			done = true
		}
	close:
		f.Close()
		if done {
			break
		}
	}
}

// 最后是实际的命令。每一个都是一个动作。

// 可以修改以进行测试。
var stdout io.Writer = os.Stdout

// printContents实现“p”命令。
func (ar *Archive) printContents(e *archive.Entry) {
	ar.extractContents1(e, stdout)
}

// tableOfContents实现了't'命令。
func (ar *Archive) tableOfContents(e *archive.Entry) {
	if ar.match(e) {
		listEntry(e, verbose)
	}
}

// extractContents实现“x”命令。
func (ar *Archive) extractContents(e *archive.Entry) {
	ar.extractContents1(e, nil)
}

func (ar *Archive) extractContents1(e *archive.Entry, out io.Writer) {
	if ar.match(e) {
		if verbose {
			listEntry(e, false)
		}
		if out == nil {
			f, err := os.OpenFile(e.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0444 /*e.Mode*/)
			if err != nil {
				log.Fatal(err)
			}
			defer f.Close()
			out = f
		}
		ar.output(e, out)
	}
}

// isGoCompilerObjFile报告文件是否是Go编译器创建的目标文件，Go编译器是一个存档文件，只有一个条目
// 。PKGDEF，或_go u。o、 或者两个条目都有。
func isGoCompilerObjFile(a *archive.Archive) bool {
	switch len(a.Entries) {
	case 1:
		return (a.Entries[0].Type == archive.EntryGoObj && a.Entries[0].Name == "_go_.o") ||
			(a.Entries[0].Type == archive.EntryPkgDef && a.Entries[0].Name == "__.PKGDEF")
	case 2:
		var foundPkgDef, foundGo bool
		for _, e := range a.Entries {
			if e.Type == archive.EntryPkgDef && e.Name == "__.PKGDEF" {
				foundPkgDef = true
			}
			if e.Type == archive.EntryGoObj && e.Name == "_go_.o" {
				foundGo = true
			}
		}
		return foundPkgDef && foundGo
	default:
		return false
	}
}
